﻿using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;
using UnityEngine.Serialization;

namespace Client
{
    /// <summary>
    /// UI射线检测
    /// </summary>
    public class UIRaycaster : BaseRaycaster
    {
        protected const int kNoEventMaskSet = -1;
        public enum BlockingObjects
        {
            None = 0,
            TwoD = 1,
            ThreeD = 2,
            All = 3,
        }

        public override int sortOrderPriority
        {
            get
            {
                return int.MaxValue - 1;
            }
        }

        public int GetSortOrderPriority(Canvas canvas)
        {
            if (canvas.renderMode == RenderMode.ScreenSpaceOverlay)
            {
                return canvas.sortingOrder;
            }
            return base.sortOrderPriority;
        }

        public override int renderOrderPriority
        {
            get
            {
                return int.MaxValue - 1;
            }
        }

        public int GetRenderOrderPriority(Canvas canvas)
        {
            if (canvas.renderMode == RenderMode.ScreenSpaceOverlay)
            {
                return canvas.renderOrder;
            }
            return base.renderOrderPriority;
        }

        [FormerlySerializedAs("ignoreReversedGraphics")]
        [SerializeField]
        private bool m_IgnoreReversedGraphics = true;
        [FormerlySerializedAs("blockingObjects")]
        [SerializeField]
        private BlockingObjects m_BlockingObjects = BlockingObjects.None;

        public bool ignoreReversedGraphics
        {
            get
            {
                return m_IgnoreReversedGraphics;
            }
            set
            {
                m_IgnoreReversedGraphics = value;
            }
        }
        public BlockingObjects blockingObjects
        {
            get
            {
                return m_BlockingObjects;
            }
            set
            {
                m_BlockingObjects = value;
            }
        }

        [SerializeField]
        protected LayerMask m_BlockingMask = kNoEventMaskSet;

        [NonSerialized]
        private Camera uiCamera;

        protected override void Start()
        {
            base.Start();
            var canvas = GetComponent<Canvas>();
            if (canvas != null)
            {
                uiCamera = canvas.worldCamera;
            }
        }

        public override Camera eventCamera
        {
            get
            {
                return uiCamera;
            }
        }

        public Camera GetCamera(Canvas canvas)
        {
            if (canvas.renderMode == RenderMode.ScreenSpaceOverlay)
            {
                return null;
            }
            return canvas.worldCamera != null ? canvas.worldCamera : Camera.main;
        }

        [NonSerialized]
        private List<Graphic> m_RaycastResults = new List<Graphic>();

        public override void Raycast(PointerEventData eventData, List<RaycastResult> resultAppendList)
        {
            if (Global.UIManager != null)
            {
                foreach (Canvas c in Global.UIManager.AllRaycastCanvas)
                {
                    RaycastCanvas(c, eventData, resultAppendList);
                }
            }
        }
        public void RaycastCanvas(Canvas canvas, PointerEventData eventData, List<RaycastResult> resultAppendList)
        {
            if (!canvas.gameObject.activeInHierarchy)
            {
                return;
            }

            // Convert to view space
            Vector2 pos;
            Camera camera = GetCamera(canvas);
            if (camera == null)
            {
                pos = new Vector2(eventData.position.x / Screen.width, eventData.position.y / Screen.height);
            }
            else
            {
                pos = camera.ScreenToViewportPoint(eventData.position);
            }

            // If it's outside the camera's viewport, do nothing
            if (pos.x < 0f || pos.x > 1f || pos.y < 0f || pos.y > 1f)
            {
                return;
            }

            Ray ray;
            if (camera != null)
            {
                ray = GetCamera(canvas).ScreenPointToRay(eventData.position);
            }
            else
            {
                ray = new Ray();
            }
            float hitDistance = CalcuHitDistance(canvas, camera, ray);

            m_RaycastResults.Clear();
            RaycastCanvas(canvas, camera, eventData.position, m_RaycastResults);

            for (var index = 0; index < m_RaycastResults.Count; index++)
            {
                var go = m_RaycastResults[index].gameObject;
                bool appendGraphic = true;

                if (ignoreReversedGraphics)
                {
                    if (camera == null)
                    {
                        // If we dont have a camera we know that we should always be facing forward
                        var dir = go.transform.rotation * Vector3.forward;
                        appendGraphic = Vector3.Dot(Vector3.forward, dir) > 0;
                    }
                    else
                    {
                        // If we have a camera compare the direction against the cameras forward.
                        var cameraFoward = camera.transform.rotation * Vector3.forward;
                        var dir = go.transform.rotation * Vector3.forward;
                        appendGraphic = Vector3.Dot(cameraFoward, dir) > 0;
                    }
                }

                if (appendGraphic)
                {
                    float distance = 0;
                    if (camera == null || canvas.renderMode == RenderMode.ScreenSpaceOverlay)
                    {
                        distance = 0;
                    }
                    else
                    {
                        Transform trans = go.transform;
                        Vector3 transForward = trans.forward;
                        // http://geomalgorithms.com/a06-_intersect-2.html
                        distance = Vector3.Dot(transForward, trans.position - ray.origin) / Vector3.Dot(transForward, ray.direction);

                        // Check to see if the go is behind the camera.
                        if (distance < 0)
                        {
                            continue;
                        }
                    }

                    if (distance >= hitDistance)
                    {
                        continue;
                    }

                    var castResult = new RaycastResult
                    {
                        gameObject = go,
                        module = this,
                        distance = distance,
                        screenPosition = eventData.position,
                        index = resultAppendList.Count,
                        depth = m_RaycastResults[index].depth,
                        sortingLayer = canvas.sortingLayerID,
                        sortingOrder = canvas.sortingOrder
                    };
                    resultAppendList.Add(castResult);
                }
            }
        }

        private float CalcuHitDistance(Canvas canvas, Camera camera, Ray ray)
        {
            float hitDistance = float.MaxValue;
            if (canvas.renderMode != RenderMode.ScreenSpaceOverlay && blockingObjects != BlockingObjects.None)
            {
                float dist = 100.0f;
                if (camera != null)
                {
                    dist = camera.farClipPlane - camera.nearClipPlane;
                }

                if (blockingObjects == BlockingObjects.ThreeD || blockingObjects == BlockingObjects.All)
                {
                    RaycastHit hit;
                    if (Physics.Raycast(ray, out hit, dist, m_BlockingMask))
                    {
                        hitDistance = hit.distance;
                    }
                }

                if (blockingObjects == BlockingObjects.TwoD || blockingObjects == BlockingObjects.All)
                {
                    RaycastHit2D hit = Physics2D.Raycast(ray.origin, ray.direction, dist, m_BlockingMask);
                    if (hit.collider != null)
                    {
                        hitDistance = hit.fraction * dist;
                    }
                }
            }
            return hitDistance;
        }

        /// <summary>
        /// Perform a raycast into the screen and collect all graphics underneath it.
        /// </summary>
        private void RaycastCanvas(Canvas canvas, Camera eventCamera, Vector2 pointerPosition, List<Graphic> results)
        {
            // Necessary for the event system
            var foundGraphics = GraphicRegistry.GetGraphicsForCanvas(canvas);
            for (int i = 0; i < foundGraphics.Count; ++i)
            {
                Graphic graphic = foundGraphics[i];

                // -1 means it hasn't been processed by the canvas, which means it isn't actually drawn
                if (graphic.depth == -1 || !graphic.raycastTarget)
                {
                    continue;
                }

                if (!RectTransformUtility.RectangleContainsScreenPoint(graphic.rectTransform, pointerPosition, eventCamera))
                {
                    continue;
                }

                if (graphic.Raycast(pointerPosition, eventCamera))
                {
                    results.Add(graphic);
                }
            }

            results.Sort((g1, g2) => g2.depth.CompareTo(g1.depth));
        }
    }
}
